/*
 *  Copyright 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc;

import org.webrtc.CameraEnumerationAndroid.CaptureFormat;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Build;
import android.os.SystemClock;
import android.util.AndroidException;
import android.util.Range;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@TargetApi(21)
public class Camera2Enumerator implements CameraEnumerator {
	private final static String TAG = "Camera2Enumerator";
	private final static double NANO_SECONDS_PER_SECOND = 1.0e9;

	// Each entry contains the supported formats for a given camera index. The formats are enumerated
	// lazily in getSupportedFormats(), and cached for future reference.
	private static final Map<String, List<CaptureFormat>> cachedSupportedFormats =
			new HashMap<String, List<CaptureFormat>>();

	final Context context;
	final CameraManager cameraManager;

	public Camera2Enumerator(Context context) {
		this.context = context;
		this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
	}

	@Override
	public String[] getDeviceNames() {
		try {
			return cameraManager.getCameraIdList();
			// On Android OS pre 4.4.2, a class will not load because of VerifyError if it contains a
			// catch statement with an Exception from a newer API, even if the code is never executed.
			// https://code.google.com/p/android/issues/detail?id=209129
		} catch (/* CameraAccessException */ AndroidException e) {
			Logging.e(TAG, "Camera access exception: " + e);
			return new String[] {};
		}
	}

	@Override
	public boolean isFrontFacing(String deviceName) {
		CameraCharacteristics characteristics = getCameraCharacteristics(deviceName);

		return characteristics != null
				&& characteristics.get(CameraCharacteristics.LENS_FACING)
				== CameraMetadata.LENS_FACING_FRONT;
	}

	@Override
	public boolean isBackFacing(String deviceName) {
		CameraCharacteristics characteristics = getCameraCharacteristics(deviceName);

		return characteristics != null
				&& characteristics.get(CameraCharacteristics.LENS_FACING)
				== CameraMetadata.LENS_FACING_BACK;
	}

	@Override
	public List<CaptureFormat> getSupportedFormats(String deviceName) {
		return getSupportedFormats(context, deviceName);
	}

	@Override
	public CameraVideoCapturer createCapturer(
			String deviceName, CameraVideoCapturer.CameraEventsHandler eventsHandler) {
		return new Camera2Capturer(context, deviceName, eventsHandler);
	}

	private CameraCharacteristics getCameraCharacteristics(String deviceName) {
		try {
			return cameraManager.getCameraCharacteristics(deviceName);
			// On Android OS pre 4.4.2, a class will not load because of VerifyError if it contains a
			// catch statement with an Exception from a newer API, even if the code is never executed.
			// https://code.google.com/p/android/issues/detail?id=209129
		} catch (/* CameraAccessException */ AndroidException e) {
			Logging.e(TAG, "Camera access exception: " + e);
			return null;
		}
	}

	/**
	 * Checks if API is supported and all cameras have better than legacy support.
	 */
	public static boolean isSupported(Context context) {
		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
			return false;
		}

		CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
		try {
			String[] cameraIds = cameraManager.getCameraIdList();
			for (String id : cameraIds) {
				CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
				if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
						== CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
					return false;
				}
			}
			// On Android OS pre 4.4.2, a class will not load because of VerifyError if it contains a
			// catch statement with an Exception from a newer API, even if the code is never executed.
			// https://code.google.com/p/android/issues/detail?id=209129
		} catch (/* CameraAccessException */ AndroidException e) {
			Logging.e(TAG, "Camera access exception: " + e);
			return false;
		}
		return true;
	}

	static int getFpsUnitFactor(Range<Integer>[] fpsRanges) {
		if (fpsRanges.length == 0) {
			return 1000;
		}
		return fpsRanges[0].getUpper() < 1000 ? 1000 : 1;
	}

	static List<Size> getSupportedSizes(CameraCharacteristics cameraCharacteristics) {
		final StreamConfigurationMap streamMap =
				cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
		final int supportLevel =
				cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

		final android.util.Size[] nativeSizes = streamMap.getOutputSizes(SurfaceTexture.class);
		final List<Size> sizes = convertSizes(nativeSizes);

		// Video may be stretched pre LMR1 on legacy implementations.
		// Filter out formats that have different aspect ratio than the sensor array.
		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1
				&& supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
			final Rect activeArraySize =
					cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
			final ArrayList<Size> filteredSizes = new ArrayList<Size>();

			for (Size size : sizes) {
				if (activeArraySize.width() * size.height == activeArraySize.height() * size.width) {
					filteredSizes.add(size);
				}
			}

			return filteredSizes;
		} else {
			return sizes;
		}
	}

	static List<CaptureFormat> getSupportedFormats(Context context, String cameraId) {
		return getSupportedFormats(
				(CameraManager) context.getSystemService(Context.CAMERA_SERVICE), cameraId);
	}

	static List<CaptureFormat> getSupportedFormats(CameraManager cameraManager, String cameraId) {
		synchronized (cachedSupportedFormats) {
			if (cachedSupportedFormats.containsKey(cameraId)) {
				return cachedSupportedFormats.get(cameraId);
			}

			Logging.d(TAG, "Get supported formats for camera index " + cameraId + ".");
			final long startTimeMs = SystemClock.elapsedRealtime();

			final CameraCharacteristics cameraCharacteristics;
			try {
				cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
			} catch (Exception ex) {
				Logging.e(TAG, "getCameraCharacteristics(): " + ex);
				return new ArrayList<CaptureFormat>();
			}

			final StreamConfigurationMap streamMap =
					cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

			Range<Integer>[] fpsRanges =
					cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
			List<CaptureFormat.FramerateRange> framerateRanges =
					convertFramerates(fpsRanges, getFpsUnitFactor(fpsRanges));
			List<Size> sizes = getSupportedSizes(cameraCharacteristics);

			int defaultMaxFps = 0;
			for (CaptureFormat.FramerateRange framerateRange : framerateRanges) {
				defaultMaxFps = Math.max(defaultMaxFps, framerateRange.max);
			}

			final List<CaptureFormat> formatList = new ArrayList<CaptureFormat>();
			for (Size size : sizes) {
				long minFrameDurationNs = 0;
				try {
					minFrameDurationNs = streamMap.getOutputMinFrameDuration(
							SurfaceTexture.class, new android.util.Size(size.width, size.height));
				} catch (Exception e) {
					// getOutputMinFrameDuration() is not supported on all devices. Ignore silently.
				}
				final int maxFps = (minFrameDurationNs == 0)
						? defaultMaxFps
						: (int) Math.round(NANO_SECONDS_PER_SECOND / minFrameDurationNs) * 1000;
				formatList.add(new CaptureFormat(size.width, size.height, 0, maxFps));
				Logging.d(TAG, "Format: " + size.width + "x" + size.height + "@" + maxFps);
			}

			cachedSupportedFormats.put(cameraId, formatList);
			final long endTimeMs = SystemClock.elapsedRealtime();
			Logging.d(TAG, "Get supported formats for camera index " + cameraId + " done."
					+ " Time spent: " + (endTimeMs - startTimeMs) + " ms.");
			return formatList;
		}
	}

	// Convert from android.util.Size to Size.
	private static List<Size> convertSizes(android.util.Size[] cameraSizes) {
		final List<Size> sizes = new ArrayList<Size>();
		for (android.util.Size size : cameraSizes) {
			sizes.add(new Size(size.getWidth(), size.getHeight()));
		}
		return sizes;
	}

	// Convert from android.util.Range<Integer> to CaptureFormat.FramerateRange.
	static List<CaptureFormat.FramerateRange> convertFramerates(
			Range<Integer>[] arrayRanges, int unitFactor) {
		final List<CaptureFormat.FramerateRange> ranges = new ArrayList<CaptureFormat.FramerateRange>();
		for (Range<Integer> range : arrayRanges) {
			ranges.add(new CaptureFormat.FramerateRange(
					range.getLower() * unitFactor, range.getUpper() * unitFactor));
		}
		return ranges;
	}
}
